//
// Copyright (c) 2009 All Right Reserved
//
// vl
//
// 2009-01-01
// Contains ...
namespace LargoCommon.Music
{
using System;
using System.Diagnostics.Contracts;
using System.Globalization;
using System.Text;
using System.Xml.Linq;
using System.Xml.Serialization;
using Abstract;
/// Tone pitch.
/// Pitch is an object for keeping given octave and element (formal pitch).
/// It allows to determine (real or formal) distances (intervals) to other pitches.
/// MidiPitch 0..127, MidiPower 0..127
[Serializable]
[XmlRoot]
public sealed class MusicalPitch : ICloneable, IComparable
{
#region Fields
/// Inner number of the lowest musical octave.
[XmlIgnore]
public const int MinOctave = 1; //// -3;
/// Inner number of the highest musical octave.
[XmlIgnore]
public const int MaxOctave = 9; // +5;
/// Referential MIDI key number.
[XmlIgnore]
public const byte BaseKeyNumber = 0; // 48; MIDI
///
/// Harmonic system.
///
private readonly HarmonicSystem harSystem;
#endregion
#region Constructors
/// Initializes a new instance of the MusicalPitch class. Serializable.
public MusicalPitch() {
}
///
/// Initializes a new instance of the MusicalPitch class.
///
/// The given system.
public MusicalPitch(HarmonicSystem givenSystem) {
Contract.Requires(givenSystem != null);
this.harSystem = givenSystem;
}
///
/// Initializes a new instance of the MusicalPitch class.
///
/// The given system.
/// Musical octave.
/// Element of system.
public MusicalPitch(HarmonicSystem givenSystem, short givenOctave, byte element) {
Contract.Requires(givenSystem != null);
this.harSystem = givenSystem;
this.Octave = givenOctave;
this.Element = element;
this.RecomputeAltitude();
}
///
/// Initializes a new instance of the MusicalPitch class.
///
/// The given system.
/// Pitch altitude.
public MusicalPitch(HarmonicSystem givenSystem, int givenAltitude) {
Contract.Requires(givenSystem != null);
this.harSystem = givenSystem;
this.SetAltitude(givenAltitude);
}
/// Initializes a new instance of the MusicalPitch class.
/// Midi Key Number.
public MusicalPitch(byte midiKeyNumber) {
this.harSystem = HarmonicSystem.GetHarmonicSystem(DefaultValue.HarmonicOrder);
this.SetAltitude(midiKeyNumber - BaseKeyNumber);
}
#endregion
#region Properties
/// Gets or sets musical octave of the pitch.
/// Property description.
public short Octave { get; set; } // MinOctave..MaxOctave
/// Gets musical octave of the pitch.
/// Middle C has StandardOctave = 4
/// Property description.
public short StandardOctave => (byte)(this.Octave - 1);
/// Gets or sets musical element of the pitch.
/// Property description.
public byte Element { get; set; } // 0..Order-1
/// Gets or sets musical element of the pitch.
/// Property description.
public int SystemAltitude { get; set; } // 0..127
/// Gets harmonic system.
/// Property description.
[XmlIgnore]
public HarmonicSystem HarmonicSystem {
get {
Contract.Ensures(Contract.Result() != null);
Contract.Ensures(Contract.Result().Order > 0);
if (this.harSystem == null) {
throw new InvalidOperationException("Harmonic system is null.");
}
return this.harSystem;
}
}
/// Gets Xml representation.
/// Property description.
public XAttribute GetXAttribute => new XAttribute("Pitch", this.SystemAltitude);
///
/// Gets MIDI key number.
///
/// Property description.
/// Returns value.
public byte MidiKeyNumber => (byte)(BaseKeyNumber + this.Halftones);
///
/// Gets Halftones - MIDI support.
///
private int Halftones {
get {
const float tolerance = 0.001f;
var totalHalftones = this.HarmonicSystem.HalftonesForInterval(this.SystemAltitude);
var roundHalfTones = (int)Math.Round(totalHalftones);
var halftones = Math.Abs(totalHalftones - roundHalfTones) < tolerance ? roundHalfTones : (int)totalHalftones;
return halftones;
}
}
#endregion
#region Static operators
//// TICS rule 7@526: Reference types should not override the equality operator (==)
//// public static bool operator ==(MusicalPitch pitch1, MusicalPitch pitch2) { return object.Equals(pitch1, pitch2); }
//// public static bool operator !=(MusicalPitch pitch1, MusicalPitch pitch2) { return !object.Equals(pitch1, pitch2); }
//// but TICS rule 7@530: Class implements interface 'IComparable' but does not implement '==' and '!='.
///
/// Implements the operator <.
///
/// The pitch1.
/// The pitch2.
///
/// Returns value.
///
public static bool operator <(MusicalPitch pitch1, MusicalPitch pitch2) {
if (pitch1 != null && pitch2 != null) {
return pitch1.SystemAltitude < pitch2.SystemAltitude;
}
return false;
}
///
/// Implements the operator >.
///
/// The pitch1.
/// The pitch2.
///
/// Returns value.
///
public static bool operator >(MusicalPitch pitch1, MusicalPitch pitch2) {
if (pitch1 != null && pitch2 != null) {
return pitch1.SystemAltitude > pitch2.SystemAltitude;
}
return false;
}
///
/// Implements the operator <=.
///
/// The pitch1.
/// The pitch2.
///
/// Returns value.
///
public static bool operator <=(MusicalPitch pitch1, MusicalPitch pitch2) {
if (pitch1 != null && pitch2 != null) {
return pitch1.SystemAltitude <= pitch2.SystemAltitude;
}
return false;
}
///
/// Implements the operator >=.
///
/// The pitch1.
/// The pitch2.
///
/// Returns value.
///
public static bool operator >=(MusicalPitch pitch1, MusicalPitch pitch2) {
if (pitch1 != null && pitch2 != null) {
return pitch1.SystemAltitude >= pitch2.SystemAltitude;
}
return false;
}
#endregion
#region Public static methods
///
/// Real Octave Number.
///
/// Musical octave.
/// Returns value.
public static int RealOctaveNumber(int octave) {
checked {
return octave - 4;
}
}
#endregion
#region Public methods
/// Makes a deep copy of the MusicalPitch object.
/// Returns object.
public object Clone() {
return new MusicalPitch(this.HarmonicSystem, this.Octave, this.Element);
}
/// Tests equality with the given pitch.
/// Musical pitch.
/// Returns value.
public bool IsEqualTo(MusicalPitch pitch) {
Contract.Requires(pitch != null);
if (pitch == null) {
return false;
}
return (this.Octave == pitch.Octave) && (this.Element == pitch.Element);
}
#endregion
#region Comparison
/// Comparer of pitches.
/// Object to be compared.
/// Returns value.
public int CompareTo(object obj) {
//// This kills the DataGrid
//// throw new ArgumentException("Object is not a MusicalPitch");
return obj is MusicalPitch mi ? this.SystemAltitude.CompareTo(mi.SystemAltitude) : 0;
}
/// Test of equality.
/// Object to be compared.
/// Returns value.
public override bool Equals(object obj) {
//// check null (this pointer is never null in C# methods)
if (object.ReferenceEquals(obj, null)) {
return false;
}
if (object.ReferenceEquals(this, obj)) {
return true;
}
if (this.GetType() != obj.GetType()) {
return false;
}
return this.CompareTo(obj) == 0;
}
/// Support of comparison.
/// Returns value.
public override int GetHashCode() {
return this.SystemAltitude.GetHashCode();
}
#endregion
#region Altitude
/// Sets the pitch location.
/// Musical octave.
/// Element of system.
public void SetValues(short octave, byte element) {
this.Octave = octave;
this.Element = element;
this.RecomputeAltitude();
}
///
/// Inverts the altitude.
///
/// The harmonic order.
public void InvertAltitude(byte harmonicOrder) {
this.SetAltitude((MaxOctave * harmonicOrder) - this.SystemAltitude);
}
/// Sets the pitch location number.
/// A real system altitude from zero.
public void SetAltitude(int givenAltitude) {
this.SystemAltitude = givenAltitude;
this.Octave = (short)(givenAltitude / this.HarmonicSystem.Order);
this.Element = (byte)(givenAltitude % this.HarmonicSystem.Order);
}
/// Returns the pitch octave location.
/// Base of musical pitch measurement.
/// Returns value.
public float OctaveAltitude(float baseAltitude) {
//// return (this.Octave + ((float)this.Element / this.HarmonicSystem.Order)) - baseAltitude;
//// Not to be exact, but quick
return this.Octave - baseAltitude;
}
///
/// Inverts the octave.
///
public void InvertOctave() {
this.Octave = (byte)(MaxOctave - this.Octave);
this.RecomputeAltitude();
}
///
/// Sets the octave.
///
/// The given octave.
public void SetOctave(int givenOctave) {
this.Octave = (byte)givenOctave;
this.RecomputeAltitude();
}
///
/// Sets the element.
///
/// The given element.
public void SetElement(int givenElement) {
this.Element = (byte)givenElement;
this.RecomputeAltitude();
}
/// Shifts octave by given number.
/// An octave shift.
public void ShiftOctave(int givenShift) {
this.Octave = (short)(this.Octave + givenShift);
this.RecomputeAltitude();
}
/// Shifts element by given number.
/// A shift of pitch element.
public void ShiftElement(int givenShift) {
this.SetAltitude(this.SystemAltitude + givenShift);
}
///
/// Moves from edges.
///
/// The min note.
/// The max note.
public void MoveFromEdges(byte minNote, byte maxNote) {
if (this.SystemAltitude < minNote) {
this.ShiftElement(minNote - this.SystemAltitude);
}
if (this.SystemAltitude > maxNote) {
this.ShiftElement(maxNote - this.SystemAltitude);
}
}
#endregion
#region Relations
/// Compute interval from the given pitch.
/// Musical pitch.
/// Returns value.
public int IntervalFrom(MusicalPitch givenPitch) {
if (givenPitch == null) {
return 0;
}
return this.SystemAltitude - givenPitch.SystemAltitude;
}
/// Compute distance from the given pitch.
/// Musical pitch.
/// Returns value.
public int DistanceFrom(MusicalPitch givenPitch) {
var interval = this.IntervalFrom(givenPitch);
Contract.Assume(interval > short.MinValue);
return Math.Abs(interval);
}
/// Compute formal distance from the given pitch.
/// Musical pitch.
/// Returns value.
public int FormalDistanceFrom(MusicalPitch givenPitch) {
var sysLength = givenPitch?.Element - this.Element ?? 0;
var interval = this.HarmonicSystem.FormalMedianLength(sysLength);
Contract.Assume(interval > short.MinValue);
var formalLength = (byte)Math.Abs(interval);
return formalLength;
}
#endregion
#region MIDI support
/// Compute MIDI pitch bend for micro intervals.
/// Returns value.
public byte MidiPitchBend() {
byte bend = 64;
if (this.HarmonicSystem.Order == DefaultValue.HarmonicOrder) {
return bend;
}
var diff = (float)(this.Halftones - Math.Floor((double)this.Halftones));
var bendDiff = (byte)Math.Floor(64 * diff);
bend += bendDiff;
return bend;
}
#endregion
#region Serialization
///
/// Sets Xml representation.
///
/// Xml Element.
public void SetXAttribute(XAttribute element) { //// XElement
Contract.Requires(element != null);
//// if (element == null) { return; }
var altitude = (int?)element; ////element.Element("Pitch");
if (altitude != null) {
this.SetAltitude((int)altitude);
}
}
#endregion
#region String representation
/// String representation of the object.
/// Returns value.
public override string ToString() {
var s = new StringBuilder();
//// s.Append(this.Element.ToString("D",System.Globalization.CultureInfo.CurrentCulture.NumberFormat));
var symbol = this.HarmonicSystem.Symbol(this.Element, true);
var normalOctave = RealOctaveNumber(this.Octave);
if (normalOctave < 0 && normalOctave > short.MinValue && symbol != null) {
symbol = symbol.ToUpper(CultureInfo.CurrentCulture);
normalOctave = -normalOctave;
}
s.Append(symbol);
s.Append(normalOctave.ToString("D", CultureInfo.CurrentCulture.NumberFormat));
return s.ToString();
}
#endregion
#region Private methods
///
/// Re-computes the altitude.
///
private void RecomputeAltitude() {
this.SystemAltitude = (this.Octave * this.HarmonicSystem.Order) + this.Element;
}
#endregion
}
}